#![allow(clippy::ref_option, clippy::needless_pass_by_value)]
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;

use anyhow::Context;
use anyhow::Result;
use anyhow::bail;
use anyhow::ensure;
use regex_lite::Regex;
use which::which;

use crate::assets;

#[cfg(target_os = "android")]
mod android {
    use super::{PermissionsExt, Result, do_cpio_cmd};
    pub(super) use crate::defs::{BACKUP_FILENAME, KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX};
    use anyhow::{Context, anyhow, bail, ensure};
    use regex_lite::Regex;
    use std::path::{Path, PathBuf};
    use std::process::{Command, Stdio};

    use crate::utils;

    pub(super) fn ensure_gki_kernel() -> Result<()> {
        let version = get_kernel_version()?;
        let is_gki = version.0 == 5 && version.1 >= 10 || version.2 > 5;
        ensure!(is_gki, "only support GKI kernel");
        Ok(())
    }

    pub fn get_kernel_version() -> Result<(i32, i32, i32)> {
        let uname = rustix::system::uname();
        let version = uname.release().to_string_lossy();
        let re = Regex::new(r"(\d+)\.(\d+)\.(\d+)")?;
        if let Some(captures) = re.captures(&version) {
            let major = captures
                .get(1)
                .and_then(|m| m.as_str().parse::<i32>().ok())
                .ok_or_else(|| anyhow!("Major version parse error"))?;
            let minor = captures
                .get(2)
                .and_then(|m| m.as_str().parse::<i32>().ok())
                .ok_or_else(|| anyhow!("Minor version parse error"))?;
            let patch = captures
                .get(3)
                .and_then(|m| m.as_str().parse::<i32>().ok())
                .ok_or_else(|| anyhow!("Patch version parse error"))?;
            Ok((major, minor, patch))
        } else {
            Err(anyhow!("Invalid kernel version string"))
        }
    }

    fn parse_kmi(version: &str) -> Result<String> {
        let re = Regex::new(r"(.* )?(\d+\.\d+)(\S+)?(android\d+)(.*)")?;
        let cap = re
            .captures(version)
            .ok_or_else(|| anyhow::anyhow!("Failed to get KMI from boot/modules"))?;
        let android_version = cap.get(4).map_or("", |m| m.as_str());
        let kernel_version = cap.get(2).map_or("", |m| m.as_str());
        Ok(format!("{android_version}-{kernel_version}"))
    }

    fn parse_kmi_from_uname() -> Result<String> {
        let uname = rustix::system::uname();
        let version = uname.release().to_string_lossy();
        parse_kmi(&version)
    }

    fn parse_kmi_from_modules() -> Result<String> {
        use std::io::BufRead;
        // find a *.ko in /vendor/lib/modules
        let modfile = std::fs::read_dir("/vendor/lib/modules")?
            .filter_map(Result::ok)
            .find(|entry| entry.path().extension().is_some_and(|ext| ext == "ko"))
            .map(|entry| entry.path())
            .ok_or_else(|| anyhow!("No kernel module found"))?;
        let output = Command::new("modinfo").arg(modfile).output()?;
        for line in output.stdout.lines().map_while(Result::ok) {
            if line.starts_with("vermagic") {
                return parse_kmi(&line);
            }
        }
        bail!("Parse KMI from modules failed")
    }

    pub fn get_current_kmi() -> Result<String> {
        parse_kmi_from_uname().or_else(|_| parse_kmi_from_modules())
    }

    fn calculate_sha1(file_path: impl AsRef<Path>) -> Result<String> {
        use sha1::Digest;
        use std::io::Read;
        let mut file = std::fs::File::open(file_path.as_ref())?;
        let mut hasher = sha1::Sha1::new();
        let mut buffer = [0; 1024];

        loop {
            let n = file.read(&mut buffer)?;
            if n == 0 {
                break;
            }
            hasher.update(&buffer[..n]);
        }

        let result = hasher.finalize();
        Ok(format!("{result:x}"))
    }

    pub(super) fn do_backup(
        magiskboot: &Path,
        workdir: &Path,
        cpio_path: &Path,
        image: &Path,
    ) -> Result<()> {
        let sha1 = calculate_sha1(image)?;
        let filename = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}");

        println!("- Backup stock boot image");
        // magiskboot cpio ramdisk.cpio 'add 0755 $BACKUP_FILENAME'
        let target = format!("{KSU_BACKUP_DIR}{filename}");
        std::fs::copy(image, &target).with_context(|| format!("backup to {target}"))?;
        std::fs::write(workdir.join(BACKUP_FILENAME), sha1.as_bytes()).context("write sha1")?;
        do_cpio_cmd(
            magiskboot,
            workdir,
            cpio_path,
            &format!("add 0755 {BACKUP_FILENAME} {BACKUP_FILENAME}"),
        )?;
        println!("- Stock image has been backup to");
        println!("- {target}");
        Ok(())
    }

    pub(super) fn clean_backup(sha1: &str) -> Result<()> {
        println!("- Clean up backup");
        let backup_name = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}");
        let dir = std::fs::read_dir(KSU_BACKUP_DIR)?;
        for entry in dir.flatten() {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            if let Some(name) = path.file_name() {
                let name = name.to_string_lossy().to_string();
                if name != backup_name
                    && name.starts_with(KSU_BACKUP_FILE_PREFIX)
                    && std::fs::remove_file(path).is_ok()
                {
                    println!("- removed {name}");
                }
            }
        }
        Ok(())
    }

    pub(super) fn flash_boot(bootdevice: &Option<String>, new_boot: PathBuf) -> Result<()> {
        let Some(bootdevice) = bootdevice else {
            bail!("boot device not found")
        };
        let status = Command::new("blockdev")
            .arg("--setrw")
            .arg(bootdevice)
            .status()?;
        ensure!(status.success(), "set boot device rw failed");
        dd(new_boot, bootdevice).context("flash boot failed")?;
        Ok(())
    }

    pub fn choose_boot_partition(
        kmi: &str,
        is_replace_kernel: bool,
        partition: &Option<String>,
    ) -> String {
        let slot_suffix = get_slot_suffix(false);
        let skip_init_boot = kmi.starts_with("android12-");
        let init_boot_exist =
            Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists();

        // if specific partition is specified, use it
        if let Some(part) = partition {
            return match part.as_str() {
                "boot" | "init_boot" | "vendor_boot" => part.clone(),
                _ => "boot".to_string(),
            };
        }

        // if init_boot exists and not skipping it, use it
        if !is_replace_kernel && init_boot_exist && !skip_init_boot {
            return "init_boot".to_string();
        }

        "boot".to_string()
    }

    pub fn get_slot_suffix(ota: bool) -> String {
        let mut slot_suffix = utils::getprop("ro.boot.slot_suffix").unwrap_or_default();
        if !slot_suffix.is_empty() && ota {
            if slot_suffix == "_a" {
                slot_suffix = "_b".to_string();
            } else {
                slot_suffix = "_a".to_string();
            }
        }
        slot_suffix
    }

    #[cfg(target_os = "android")]
    pub fn list_available_partitions() -> Vec<String> {
        let slot_suffix = get_slot_suffix(false);
        let candidates = vec!["boot", "init_boot", "vendor_boot"];
        candidates
            .into_iter()
            .filter(|name| Path::new(&format!("/dev/block/by-name/{name}{slot_suffix}")).exists())
            .map(ToString::to_string)
            .collect()
    }

    #[cfg(target_os = "android")]
    pub(super) fn post_ota() -> Result<()> {
        use crate::assets::BOOTCTL_PATH;
        use crate::defs::ADB_DIR;
        let status = Command::new(BOOTCTL_PATH).arg("hal-info").status()?;
        if !status.success() {
            return Ok(());
        }

        let current_slot = Command::new(BOOTCTL_PATH)
            .arg("get-current-slot")
            .output()?
            .stdout;
        let current_slot = String::from_utf8(current_slot)?;
        let current_slot = current_slot.trim();
        let target_slot = i32::from(current_slot == "0");

        Command::new(BOOTCTL_PATH)
            .arg(format!("set-active-boot-slot {target_slot}"))
            .status()?;

        let post_fs_data = Path::new(ADB_DIR).join("post-fs-data.d");
        utils::ensure_dir_exists(&post_fs_data)?;
        let post_ota_sh = post_fs_data.join("post_ota.sh");

        let sh_content = format!(
            r"
{BOOTCTL_PATH} mark-boot-successful
rm -f {BOOTCTL_PATH}
rm -f /data/adb/post-fs-data.d/post_ota.sh
"
        );

        std::fs::write(&post_ota_sh, sh_content)?;
        #[cfg(unix)]
        std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?;

        Ok(())
    }

    pub(super) fn dd<P: AsRef<Path>, Q: AsRef<Path>>(ifile: P, ofile: Q) -> Result<()> {
        let status = Command::new("dd")
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .arg(format!("if={}", ifile.as_ref().display()))
            .arg(format!("of={}", ofile.as_ref().display()))
            .status()?;
        ensure!(
            status.success(),
            "dd if={:?} of={:?} failed",
            ifile.as_ref(),
            ofile.as_ref()
        );
        Ok(())
    }
}

#[cfg(target_os = "android")]
pub use android::*;

fn parse_kmi_from_kernel(kernel: &PathBuf, workdir: &Path) -> Result<String> {
    use std::fs::{File, copy};
    use std::io::{BufReader, Read};
    let kernel_path = workdir.join("kernel");
    copy(kernel, &kernel_path).context("Failed to copy kernel")?;

    let file = File::open(&kernel_path).context("Failed to open kernel file")?;
    let mut reader = BufReader::new(file);
    let mut buffer = Vec::new();
    reader
        .read_to_end(&mut buffer)
        .context("Failed to read kernel file")?;

    let printable_strings: Vec<&str> = buffer
        .split(|&b| b == 0)
        .filter_map(|slice| std::str::from_utf8(slice).ok())
        .filter(|s| s.chars().all(|c| c.is_ascii_graphic() || c == ' '))
        .collect();

    let re =
        Regex::new(r"(?:.* )?(\d+\.\d+)(?:\S+)?(android\d+)").context("Failed to compile regex")?;
    for s in printable_strings {
        if let Some(caps) = re.captures(s)
            && let (Some(kernel_version), Some(android_version)) = (caps.get(1), caps.get(2))
        {
            let kmi = format!("{}-{}", android_version.as_str(), kernel_version.as_str());
            return Ok(kmi);
        }
    }
    println!("- Failed to get KMI version");
    bail!("Try to choose LKM manually")
}

fn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Result<String> {
    let image_path = workdir.join("image");

    std::fs::copy(image, &image_path).context("Failed to copy image")?;

    let status = Command::new(magiskboot)
        .current_dir(workdir)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .arg("unpack")
        .arg(&image_path)
        .status()
        .context("Failed to execute magiskboot command")?;

    if !status.success() {
        bail!("magiskboot unpack failed with status: {:?}", status);
    }

    parse_kmi_from_kernel(&image_path, workdir)
}

fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cpio_path: &Path, cmd: &str) -> Result<()> {
    let status = Command::new(magiskboot)
        .current_dir(workdir)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .arg("cpio")
        .arg(cpio_path)
        .arg(cmd)
        .status()?;
    ensure!(status.success(), "magiskboot cpio {} failed", cmd);
    Ok(())
}

fn is_magisk_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {
    let status = Command::new(magiskboot)
        .current_dir(workdir)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .arg("cpio")
        .arg(cpio_path)
        .arg("test")
        .status()?;
    // 0: stock, 1: magisk
    Ok(status.code() == Some(1))
}

fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {
    let status = Command::new(magiskboot)
        .current_dir(workdir)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .arg("cpio")
        .arg(cpio_path)
        .arg("exists kernelsu.ko")
        .status()?;

    Ok(status.success())
}

fn find_magiskboot(magiskboot_path: Option<PathBuf>, workdir: &Path) -> Result<PathBuf> {
    let magiskboot = {
        if which("magiskboot").is_ok() {
            #[cfg(target_os = "android")]
            let _ = assets::ensure_binaries(true);
            "magiskboot".into()
        } else {
            // magiskboot is not in $PATH, use builtin or specified one
            let magiskboot = if let Some(magiskboot_path) = magiskboot_path {
                std::fs::canonicalize(magiskboot_path)?
            } else {
                let magiskboot_path = workdir.join("magiskboot");
                assets::copy_assets_to_file("magiskboot", &magiskboot_path)
                    .context("copy magiskboot failed")?;
                magiskboot_path
            };
            ensure!(magiskboot.exists(), "{magiskboot:?} is not exist");
            #[cfg(unix)]
            let _ = std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755));
            magiskboot
        }
    };
    Ok(magiskboot)
}

fn find_boot_image(
    image: &Option<PathBuf>,
    kmi: &str,
    ota: bool,
    is_replace_kernel: bool,
    workdir: &Path,
    partition: &Option<String>,
) -> Result<(PathBuf, Option<String>)> {
    let bootimage;
    let mut bootdevice = None;
    if let Some(ref image) = *image {
        ensure!(image.exists(), "boot image not found");
        bootimage = std::fs::canonicalize(image)?;
    } else {
        #[cfg(not(target_os = "android"))]
        {
            println!("- Current OS is not android, refusing auto bootimage/bootdevice detection");
            bail!("Please specify a boot image");
        }

        #[cfg(target_os = "android")]
        {
            let slot_suffix = get_slot_suffix(ota);
            let boot_partition_name = choose_boot_partition(kmi, is_replace_kernel, partition);
            let boot_partition = format!("/dev/block/by-name/{boot_partition_name}{slot_suffix}");

            println!("- Bootdevice: {boot_partition}");
            let tmp_boot_path = workdir.join("boot.img");

            dd(&boot_partition, &tmp_boot_path)?;

            ensure!(tmp_boot_path.exists(), "boot image not found");

            bootimage = tmp_boot_path;
            bootdevice = Some(boot_partition);
        }
    }
    Ok((bootimage, bootdevice))
}

#[derive(clap::Args, Debug)]
pub struct BootPatchArgs {
    /// boot image path, if not specified, will try to find the boot image automatically
    #[arg(short, long)]
    pub boot: Option<PathBuf>,

    /// kernel image path to replace
    #[arg(short, long)]
    pub kernel: Option<PathBuf>,

    /// LKM module path to replace, if not specified, will use the builtin one
    #[arg(short, long)]
    pub module: Option<PathBuf>,

    /// init to be replaced
    #[arg(short, long, requires("module"))]
    pub init: Option<PathBuf>,

    /// will use another slot when boot image is not specified
    #[cfg(target_os = "android")]
    #[arg(short = 'u', long, default_value = "false")]
    pub ota: bool,

    /// Flash it to boot partition after patch
    #[cfg(target_os = "android")]
    #[arg(short, long, default_value = "false")]
    pub flash: bool,

    /// output path, if not specified, will use current directory
    #[arg(short, long, default_value = None)]
    pub out: Option<PathBuf>,

    /// magiskboot path, if not specified, will search from $PATH
    #[arg(long, default_value = None)]
    pub magiskboot: Option<PathBuf>,

    /// KMI version, if specified, will use the specified KMI
    #[arg(long, default_value = None)]
    pub kmi: Option<String>,

    /// target partition override (init_boot | boot | vendor_boot)
    #[cfg(target_os = "android")]
    #[arg(long, default_value = None)]
    pub partition: Option<String>,

    /// File name of the output.
    #[arg(long, default_value = None)]
    pub out_name: Option<String>,
}

pub fn patch(args: BootPatchArgs) -> Result<()> {
    let inner = move || {
        let BootPatchArgs {
            boot: image,
            init,
            kernel,
            module: kmod,
            out,
            magiskboot: magiskboot_path,
            kmi,
            out_name,
            ..
        } = args;
        #[cfg(target_os = "android")]
        let BootPatchArgs {
            ota,
            flash,
            partition,
            ..
        } = args;

        println!(include_str!("banner"));

        let patch_file = image.is_some();

        #[cfg(target_os = "android")]
        if !patch_file {
            ensure_gki_kernel()?;
        }

        let is_replace_kernel = kernel.is_some();

        if is_replace_kernel {
            ensure!(
                init.is_none() && kmod.is_none(),
                "init and module must not be specified."
            );
        }

        let tmpdir = tempfile::Builder::new()
            .prefix("KernelSU")
            .tempdir()
            .context("create temp dir failed")?;
        let workdir = tmpdir.path();

        // extract magiskboot
        let magiskboot = find_magiskboot(magiskboot_path, workdir)?;

        let kmi = kmi.map_or_else(
            || -> Result<_> {
                #[cfg(target_os = "android")]
                match get_current_kmi() {
                    Ok(value) => {
                        return Ok(value);
                    }
                    Err(e) => {
                        println!("- {e}");
                    }
                }
                Ok(if let Some(image_path) = &image {
                    println!(
                        "- Trying to auto detect KMI version for {}",
                        image_path.display()
                    );
                    parse_kmi_from_boot(&magiskboot, image_path, tmpdir.path())?
                } else if let Some(kernel_path) = &kernel {
                    println!(
                        "- Trying to auto detect KMI version for {}",
                        kernel_path.display()
                    );
                    parse_kmi_from_kernel(kernel_path, tmpdir.path())?
                } else {
                    String::new()
                })
            },
            Ok,
        )?;

        #[cfg(target_os = "android")]
        let (bootimage, bootdevice) =
            find_boot_image(&image, &kmi, ota, is_replace_kernel, workdir, &partition)?;

        #[cfg(not(target_os = "android"))]
        let (bootimage, _) =
            find_boot_image(&image, &kmi, false, is_replace_kernel, workdir, &None)?;

        let bootimage = bootimage.as_path();

        // try extract magiskboot/bootctl
        #[cfg(target_os = "android")]
        let _ = assets::ensure_binaries(false);

        if let Some(kernel) = kernel {
            std::fs::copy(kernel, workdir.join("kernel")).context("copy kernel from failed")?;
        }

        println!("- Preparing assets");

        let kmod_file = workdir.join("kernelsu.ko");
        if let Some(kmod) = kmod {
            std::fs::copy(kmod, kmod_file).context("copy kernel module failed")?;
        } else {
            // If kmod is not specified, extract from assets
            println!("- KMI: {kmi}");
            let name = format!("{kmi}_kernelsu.ko");
            assets::copy_assets_to_file(&name, kmod_file)
                .with_context(|| format!("Failed to copy {name}"))?;
        }

        let init_file = workdir.join("init");
        if let Some(init) = init {
            std::fs::copy(init, init_file).context("copy init failed")?;
        } else {
            assets::copy_assets_to_file("ksuinit", init_file).context("copy ksuinit failed")?;
        }

        println!("- Unpacking boot image");
        let status = Command::new(&magiskboot)
            .current_dir(workdir)
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .arg("unpack")
            .arg(bootimage)
            .status()?;
        ensure!(status.success(), "magiskboot unpack failed");

        let mut ramdisk = workdir.join("ramdisk.cpio");
        if !ramdisk.exists() {
            ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio");
        }
        if !ramdisk.exists() {
            ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio");
        }
        if !ramdisk.exists() {
            println!("- No ramdisk, create by default");
            ramdisk = "ramdisk.cpio".into();
        }
        let ramdisk = ramdisk.as_path();
        let is_magisk_patched = is_magisk_patched(&magiskboot, workdir, ramdisk)?;
        ensure!(!is_magisk_patched, "Cannot work with Magisk patched image");

        println!("- Adding KernelSU LKM");
        let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;

        if !is_kernelsu_patched {
            // kernelsu.ko is not exist, backup init if necessary
            let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init");
            if status.is_ok() {
                do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init init.real")?;
            }
        }

        do_cpio_cmd(&magiskboot, workdir, ramdisk, "add 0755 init init")?;
        do_cpio_cmd(
            &magiskboot,
            workdir,
            ramdisk,
            "add 0755 kernelsu.ko kernelsu.ko",
        )?;

        #[cfg(target_os = "android")]
        if !is_kernelsu_patched
            && flash
            && let Err(e) = do_backup(&magiskboot, workdir, ramdisk, bootimage)
        {
            println!("- Backup stock image failed: {e}");
        }

        println!("- Repacking boot image");
        // magiskboot repack boot.img
        let status = Command::new(&magiskboot)
            .current_dir(workdir)
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .arg("repack")
            .arg(bootimage)
            .status()?;
        ensure!(status.success(), "magiskboot repack failed");
        let new_boot = workdir.join("new-boot.img");

        if patch_file {
            // if image is specified, write to output file
            let output_dir = out.unwrap_or(std::env::current_dir()?);
            let name = out_name.unwrap_or_else(|| {
                let now = chrono::Utc::now();
                format!("kernelsu_patched_{}.img", now.format("%Y%m%d_%H%M%S"))
            });
            let output_image = output_dir.join(name);
            if std::fs::rename(&new_boot, &output_image).is_err() {
                std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
            }
            println!("- Output file is written to");
            println!("- {}", output_image.display().to_string().trim_matches('"'));
        }

        #[cfg(target_os = "android")]
        if flash {
            println!("- Flashing new boot image");
            flash_boot(&bootdevice, new_boot)?;

            if ota {
                post_ota()?;
            }
        }

        println!("- Done!");
        Ok(())
    };

    let result = inner();
    if let Err(ref e) = result {
        println!("- Patch Error: {e}");
    }
    result
}

#[derive(clap::Args, Debug)]
pub struct BootRestoreArgs {
    /// boot image path, if not specified, will try to find the boot image automatically
    #[arg(short, long)]
    pub boot: Option<PathBuf>,

    /// Flash it to boot partition after restore
    #[cfg(target_os = "android")]
    #[arg(short, long, default_value = "false")]
    pub flash: bool,

    /// magiskboot path, if not specified, will search from $PATH
    #[arg(long, default_value = None)]
    pub magiskboot: Option<PathBuf>,

    /// File name of the output.
    #[arg(long, default_value = None)]
    pub out_name: Option<String>,
}

pub fn restore(args: BootRestoreArgs) -> Result<()> {
    let BootRestoreArgs {
        boot: image,
        magiskboot: magiskboot_path,
        out_name,
        ..
    } = args;
    #[cfg(target_os = "android")]
    let BootRestoreArgs { flash, .. } = args;

    let tmpdir = tempfile::Builder::new()
        .prefix("KernelSU")
        .tempdir()
        .context("create temp dir failed")?;
    let workdir = tmpdir.path();
    let magiskboot = find_magiskboot(magiskboot_path, workdir)?;

    #[cfg(target_os = "android")]
    let kmi = get_current_kmi().unwrap_or_default();

    #[cfg(target_os = "android")]
    let (bootimage, bootdevice) = find_boot_image(&image, &kmi, false, false, workdir, &None)?;
    #[cfg(not(target_os = "android"))]
    let (bootimage, _) = find_boot_image(&image, "", false, false, workdir, &None)?;

    println!("- Unpacking boot image");
    let status = Command::new(&magiskboot)
        .current_dir(workdir)
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .arg("unpack")
        .arg(bootimage.display().to_string())
        .status()?;
    ensure!(status.success(), "magiskboot unpack failed");

    let mut ramdisk = workdir.join("ramdisk.cpio");
    if !ramdisk.exists() {
        ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio");
    }
    if !ramdisk.exists() {
        ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio");
    }
    if !ramdisk.exists() {
        bail!("No compatible ramdisk found.")
    }
    let ramdisk = ramdisk.as_path();
    let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;
    ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU");

    #[cfg(target_os = "android")]
    let mut new_boot = None;
    #[cfg(target_os = "android")]
    let mut from_backup = false;
    #[cfg(not(target_os = "android"))]
    let from_backup = false;

    #[cfg(target_os = "android")]
    if do_cpio_cmd(
        &magiskboot,
        workdir,
        ramdisk,
        &format!("exists {BACKUP_FILENAME}"),
    )
    .is_ok()
    {
        do_cpio_cmd(
            &magiskboot,
            workdir,
            ramdisk,
            &format!("extract {BACKUP_FILENAME} {BACKUP_FILENAME}"),
        )?;
        let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?;
        let sha = String::from_utf8(sha)?;
        let sha = sha.trim();
        let backup_path =
            PathBuf::from(KSU_BACKUP_DIR).join(format!("{KSU_BACKUP_FILE_PREFIX}{sha}"));
        if backup_path.is_file() {
            new_boot = Some(backup_path);
            from_backup = true;
        } else {
            println!("- Warning: no backup {} found!", backup_path.display());
        }

        if let Err(e) = clean_backup(sha) {
            println!("- Warning: Cleanup backup image failed: {e}");
        }
    } else {
        println!("- Backup info is absent!");
    }

    let remove_ksu = || -> Result<_> {
        // remove kernelsu.ko
        do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm kernelsu.ko")?;

        // if init.real exists, restore it
        let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init.real").is_ok();
        if status {
            do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init.real init")?;
        }

        println!("- Repacking boot image");
        let status = Command::new(&magiskboot)
            .current_dir(workdir)
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .arg("repack")
            .arg(&bootimage)
            .status()?;
        ensure!(status.success(), "magiskboot repack failed");
        Ok(workdir.join("new-boot.img"))
    };
    #[cfg(target_os = "android")]
    let new_boot = new_boot.map_or_else(remove_ksu, Ok)?;
    #[cfg(not(target_os = "android"))]
    let new_boot = remove_ksu()?;

    if image.is_some() {
        // if image is specified, write to output file
        let output_dir = std::env::current_dir()?;
        let name = out_name.unwrap_or_else(|| {
            let now = chrono::Utc::now();
            format!("kernelsu_restore_{}.img", now.format("%Y%m%d_%H%M%S"))
        });
        let output_image = output_dir.join(name);

        if from_backup || std::fs::rename(&new_boot, &output_image).is_err() {
            std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?;
        }
        println!("- Output file is written to");
        println!("- {}", output_image.display().to_string().trim_matches('"'));
    }
    #[cfg(target_os = "android")]
    if flash {
        if from_backup {
            println!("- Flashing new boot image from {}", new_boot.display());
        } else {
            println!("- Flashing new boot image");
        }
        flash_boot(&bootdevice, new_boot)?;
    }
    println!("- Done!");
    Ok(())
}
